/* * Copyright 2008-2010 Xebia and the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package fr.xebia.management.statistics; import java.lang.reflect.Method; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import javax.management.MBeanServer; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParserContext; import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.jmx.export.MBeanExporter; import org.springframework.jmx.export.annotation.AnnotationMBeanExporter; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.jmx.export.naming.SelfNaming; import org.springframework.jmx.support.JmxUtils; import org.springframework.util.StringUtils; /** * <p> * Aspect to handle the methods annotated with the {@link Profiled} annotation. * </p> * * @author <a href="mailto:cyrille@cyrilleleclerc.com">Cyrille Le Clerc</a> */ @ManagedResource @Aspect public class ProfileAspect implements InitializingBean, DisposableBean, BeanNameAware, SelfNaming { public enum ClassNameStyle { COMPACT_FULLY_QUALIFIED_NAME, FULLY_QUALIFIED_NAME, SHORT_NAME }; protected static final class RootObject { private final ProceedingJoinPoint pjp; private RootObject(ProceedingJoinPoint pjp) { super(); this.pjp = pjp; } public Object[] getArgs() { return pjp.getArgs(); } public Object getInvokedObject() { return pjp.getThis(); } public Properties getSystemProperties() { return System.getProperties(); } } /** * <p> * Formats the given <code>fullyQualifiedName</code> according to the given * <code>classNameStyle</code>. * </p> * <p> * Samples with <code>java.lang.String</code>: * <ul> * <li>{@link ClassNameStyle#FULLY_QUALIFIED_NAME} : * <code>java.lang.String</code></li> * <li>{@link ClassNameStyle#COMPACT_FULLY_QUALIFIED_NAME} : * <code>j.l.String</code></li> * <li>{@link ClassNameStyle#SHORT_NAME} : <code>String</code></li> * </ul> * </p> */ protected static String getFullyQualifiedMethodName(String fullyQualifiedClassName, String methodName, ClassNameStyle classNameStyle) { StringBuilder fullyQualifiedMethodName = new StringBuilder(fullyQualifiedClassName.length() + methodName.length() + 1); switch (classNameStyle) { case FULLY_QUALIFIED_NAME: fullyQualifiedMethodName.append(fullyQualifiedClassName); break; case COMPACT_FULLY_QUALIFIED_NAME: String[] splittedFullyQualifiedName = StringUtils.delimitedListToStringArray(fullyQualifiedClassName, "."); for (int i = 0; i < splittedFullyQualifiedName.length - 1; i++) { fullyQualifiedMethodName.append(splittedFullyQualifiedName[i].charAt(0)).append("."); } fullyQualifiedMethodName.append(splittedFullyQualifiedName[splittedFullyQualifiedName.length - 1]); break; case SHORT_NAME: fullyQualifiedMethodName.append(StringUtils.unqualify(fullyQualifiedClassName)); break; default: // should not occur fullyQualifiedMethodName.append(fullyQualifiedClassName); break; } fullyQualifiedMethodName.append(".").append(methodName); return fullyQualifiedMethodName.toString(); } private final Logger logger = LoggerFactory.getLogger(getClass()); private ClassNameStyle classNameStyle = ClassNameStyle.COMPACT_FULLY_QUALIFIED_NAME; private ExpressionParser expressionParser = new SpelExpressionParser(); /** * @see ObjectName#getDomain() */ private String jmxDomain = "fr.xebia"; private MBeanExporter mbeanExporter; private String name; private ObjectName objectName; private ParserContext parserContext = new TemplateParserContext(); private ConcurrentMap<Method, Expression> profiledMethodNameAsExpressionByMethod = new ConcurrentHashMap<Method, Expression>(); private MBeanServer server; /** * visible for tests */ protected ConcurrentMap<String, ServiceStatistics> serviceStatisticsByName = new ConcurrentHashMap<String, ServiceStatistics>(); public void afterPropertiesSet() throws Exception { if (this.server == null) { this.server = JmxUtils.locateMBeanServer(); } this.mbeanExporter = new AnnotationMBeanExporter(); this.mbeanExporter.setEnsureUniqueRuntimeObjectNames(false); this.mbeanExporter.setServer(this.server); this.mbeanExporter.setAutodetectMode(MBeanExporter.AUTODETECT_NONE); this.mbeanExporter.afterPropertiesSet(); } public void destroy() throws Exception { this.mbeanExporter.destroy(); } public MBeanExporter getMbeanExporter() { return mbeanExporter; } public ObjectName getObjectName() throws MalformedObjectNameException { if (objectName == null) { String objectNameAsString = jmxDomain + ":type=ProfileAspect"; if (StringUtils.hasLength(name)) { objectNameAsString += ",name=" + ObjectName.quote(name); } objectName = new ObjectName(objectNameAsString); } return objectName; } @ManagedAttribute public int getRegisteredServiceStatisticsCount() { return this.serviceStatisticsByName.size(); } @Around(value = "execution(* *(..)) && @annotation(profiled)", argNames = "pjp,profiled") public Object profileInvocation(ProceedingJoinPoint pjp, Profiled profiled) throws Throwable { logger.trace("> profileInvocation({},{}", pjp, profiled); MethodSignature jointPointSignature = (MethodSignature) pjp.getStaticPart().getSignature(); // COMPUTE SERVICE STATISTICS NAME Expression nameAsExpression = profiledMethodNameAsExpressionByMethod.get(jointPointSignature.getMethod()); if (nameAsExpression == null) { if (StringUtils.hasLength(profiled.name())) { String nameAsStringExpression = profiled.name(); nameAsExpression = expressionParser.parseExpression(nameAsStringExpression, parserContext); } else { String fullyQualifiedMethodName = getFullyQualifiedMethodName(// jointPointSignature.getDeclaringTypeName(), // jointPointSignature.getName(), // this.classNameStyle); nameAsExpression = new LiteralExpression(fullyQualifiedMethodName); } } String serviceStatisticsName; if (nameAsExpression instanceof LiteralExpression) { // Optimization : prevent useless objects instantiations serviceStatisticsName = nameAsExpression.getExpressionString(); } else { serviceStatisticsName = nameAsExpression.getValue(new RootObject(pjp), String.class); } // LOOKUP SERVICE STATISTICS ServiceStatistics serviceStatistics = serviceStatisticsByName.get(serviceStatisticsName); if (serviceStatistics == null) { // INSTIANCIATE NEW SERVICE STATISTICS ServiceStatistics newServiceStatistics = new ServiceStatistics(// new ObjectName(this.jmxDomain + ":type=ServiceStatistics,name=" + serviceStatisticsName), // profiled.businessExceptionsTypes(), profiled.communicationExceptionsTypes()); newServiceStatistics.setSlowInvocationThresholdInMillis(profiled.slowInvocationThresholdInMillis()); newServiceStatistics.setVerySlowInvocationThresholdInMillis(profiled.verySlowInvocationThresholdInMillis()); int maxActive; if (StringUtils.hasLength(profiled.maxActiveExpression())) { maxActive = expressionParser.parseExpression(profiled.maxActiveExpression(), parserContext).getValue(new RootObject(pjp), Integer.class); } else { maxActive = profiled.maxActive(); } newServiceStatistics.setMaxActive(maxActive); newServiceStatistics.setMaxActiveSemaphoreAcquisitionMaxTimeInNanos( TimeUnit.NANOSECONDS.convert(profiled.maxActiveSemaphoreAcquisitionMaxTimeInMillis(), TimeUnit.MILLISECONDS)); ServiceStatistics previousServiceStatistics = serviceStatisticsByName.putIfAbsent(serviceStatisticsName, newServiceStatistics); if (previousServiceStatistics == null) { serviceStatistics = newServiceStatistics; mbeanExporter.registerManagedResource(serviceStatistics); } else { serviceStatistics = previousServiceStatistics; } } // INVOKE AND PROFILE INVOCATION long nanosBefore = System.nanoTime(); Semaphore semaphore = serviceStatistics.getMaxActiveSemaphore(); if (semaphore != null) { boolean acquired = semaphore.tryAcquire(serviceStatistics.getMaxActiveSemaphoreAcquisitionMaxTimeInNanos(), TimeUnit.NANOSECONDS); if (!acquired) { serviceStatistics.incrementServiceUnavailableExceptionCount(); throw new ServiceUnavailableException("Service '" + serviceStatisticsName + "' is unavailable: " + serviceStatistics.getCurrentActive() + " invocations of are currently running"); } } serviceStatistics.incrementCurrentActiveCount(); try { Object returned = pjp.proceed(); return returned; } catch (Throwable t) { serviceStatistics.incrementExceptionCount(t); throw t; } finally { if (semaphore != null) { semaphore.release(); } serviceStatistics.decrementCurrentActiveCount(); long deltaInNanos = System.nanoTime() - nanosBefore; serviceStatistics.incrementInvocationCounterAndTotalDurationWithNanos(deltaInNanos); if (logger.isDebugEnabled()) { logger.debug("< profileInvocation({}): {}ns", serviceStatisticsName, deltaInNanos); } } } public void setBeanName(String beanName) { this.name = beanName; } public void setClassNameStyle(ClassNameStyle classNameStyle) { this.classNameStyle = classNameStyle; } /** * * @param classNameStyle * one of COMPACT_FULLY_QUALIFIED_NAME, FULLY_QUALIFIED_NAME and * SHORT_NAME */ public void setClassNameStyle(String classNameStyle) { this.classNameStyle = ClassNameStyle.valueOf(classNameStyle); } public void setJmxDomain(String jmxDomain) { this.jmxDomain = jmxDomain; } public void setServer(MBeanServer server) { this.server = server; } }